上一篇使用 <AnimatePresence>
都是直接底下包裹 motion 元件, 實際上我們更常把元件打包起來成客製化元件 (<Item> 這樣)
,如果想在元件使用 <AnimatePresence>
有些要注意的地方。今天也會加上 react-router做頁面轉場的效果,有了轉場動畫,整個體驗就很不一樣了。
如果要應用離場動畫,至少內部要有一個是 motion 元件並且含有 exit prop。另一個要求自製元件一定是 AnimatePresence
的下一層 (direct descendant),什麼意思呢 ?
一定要包 DOM 節點消失的那個物件,不是內層的物件。
// 可以
export const MyComponent = ({ items }) => (
<AnimatePresence>
// 下一層
{items.map(({ id }) => (
<Item key={id} />
))}
</AnimatePresence>
)
// 不行
export const MyComponent = ({ items }) => (
<AnimatePresence>
// 下一層
<div key={id} >
{items.map(({ id }) => (
<Item />
))}
</div>
</AnimatePresence>
)
主要達成只有兩個 :
會使用到 ReactDOM 的 createPortal
,其原因 React 官方說明的很明確 :
一個典型的 portal 使用案例是,當 parent component 有 overflow: hidden 或者 z-index 的樣式時,卻仍需要 child 在視覺上「跳出」其容器的狀況。例如 dialog、hovercard 與 tooltip 都屬於此案例。
基本知識了解後就來實作了 :D。
useToggle
: 之後有關 開關的元件就直接使用了,在 /Hooks/useToogle
import { useState } from "react";
export default function useToggle(initialType = false) {
const [toggle, setToggle] = useState(initialType);
const handleToggle = (boolean) => (e) => {
// 什麼都不傳或 null 就是開開關關
if (boolean === null || boolean === undefined) {
setToggle((boolean) => !boolean);
return;
}
// 指定操作
setToggle(boolean);
};
return {
toggle,
handleToggle,
};
}
public/index.html
加上等等要 portal 的容器<!-- 一般元件的生長的位置 -->
<div id="root"></div>
<!-- 拿來裝 modal 的容器 -->
<div id="modal-root"></div>
// CSS
.modal-Box{
position: absolute;
top:0;
left:0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
}
.modal-content {
background-color: #fefefe;
margin: 15% auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
z-index: 1;
}
.close {
color: #aaa;
float: right;
font-size: 32px;
font-weight: bold;
}
.overlay{
position: absolute;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.4);
}
// JS
// 引入 動畫元件
import { AnimatePresence, motion } from "framer-motion";
import React from "react";
// 引入 ReactDOM 要使用 createPortal
import ReactDOM from "react-dom";
// 引入 Hooks
import useToggle from "../../../Hooks/useToggle";
// 引入動畫的 variants
import { modalVariants, modalBoxVariants } from "./animate";
import "./Modal.style.css";
function Modal() {
// 冒號只是另外取名而已
const { toggle: open, handleToggle: handleOpen } = useToggle(false);
return (
<>
// 因為要展示,所以外部有一個作為開關用的
<button onClick={handleOpen(true)}>
Open Modal
</button>
{/* 離場動畫判斷 */}
<AnimatePresence initial={false}>
// 把 function 傳進入
{open && <ModalBox handleOpen={handleOpen} />}
</AnimatePresence>
</>
);
}
const ModalBox = ({ handleOpen }) => {
return ReactDOM.createPortal(
<div className="modal-Box">
// 占全版的黑幕
<motion.div
className="overlay"
onClick={handleOpen(false)}
variants={modalVariants}
initial="hidden"
animate="show"
exit="hidden"
/>
// modal 本體
<motion.div
className="modal-content"
variants={modalBoxVariants}
initial="hidden"
animate="show"
exit="hidden"
>
<div className="modal-header">
// 叉叉加入事件與 hover 動畫
<motion.span
className="close"
onClick={handleOpen(false)}
whileHover={{
color: "red",
cursor: "pointer",
}}
>
×
</motion.span>
<h2>Modal Header</h2>
</div>
<div className="modal-body">
<p>Some text in the Modal Body</p>
<p>Some other text...</p>
</div>
<div className="modal-footer">
<h3>Modal Footer</h3>
</div>
</motion.div>
</div>,
document.getElementById("modal-root")
);
};
更客製化的話可以定義一些 props 跟在 motion 元件上使用 custom
,傳入動態的動畫數值。
react-router 我用過次數大概一隻手可以算出來,如果有誤的話,再麻煩高手指點一下 Q
如果是在個人專案使用,這段可以跳過,並到下面實作的部分。
npm install react-router-dom@6
src/index.js
最外處加入 <BrowserRouter>
元件,設置好路由起點// 引入 react-router-dom
import { BrowserRouter } from "react-router-dom";
// 主要部分
root.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
src/App.js
加上 <Routes>
並且設好 <Route>
import { Routes, Route } from "react-router-dom";
function App() {
const [day, setDay] = useState(Object.keys(Days).length + 1);
return (
<div>
<label htmlFor="days">鐵人賽第 {day} 天</label>
<select
id="days"
value={day}
onChange={(e) => setDay(e.target.value)}
>
{Object.keys(Days).map((_, i) => {
return (
<option key={i} value={i + 2}>
Day {i + 2}
</option>
);
})}
</select>
{/* 因路徑改變的元件在這裡~ */}
<Routes>
{/* :slug 是對應網址 */}
<Route path="Day:day" element={<DailyTemplate />} />
</Routes>
</div>
);
}
<Link>
元件做導向,但這邊我是用下拉選單選取,所以用 usenavigate
// 引入 useNavigate
import { Routes, Route, useNavigate } from "react-router-dom";
const navigate = useNavigate(); // 加上 Hooks
// 當 day 改變時就導到對應網址
useEffect(() => {
navigate(`/Day${day}`);
}, [day, navigate]);
:day
來決定載入的 animation 元件 ,在 template/DailyTemplate.js
補上import { useParams } from "react-router-dom"; // 引入 Hooks
let { day } = useParams(); // 抓網址後的參數
到這邊我們完成了基本的設置,我的架構都一改再改,每天打開看就是手很癢阿阿阿 !
// 引入 useLocation
import { Routes, Route, useNavigate, useLocation } from "react-router-dom";
const location = useLocation();
// 加上 AnimatePresence 並且取消第一次的 initial,mode="wait" 會等到所有的 exit 結束
<AnimatePresence initial={false} mode="wait">
// 為 Routes 加上 key 每次都能刷新
<Routes key={location.key} location={location}>
<Route path="Day:day" element={<DailyTemplate />} />
</Routes>
</AnimatePresence>
const containerVariants = {
exit: {
top: ["100%", "0%", "0%"],
bottom: ["0%", "0%", "100%"],
transition: {
type: "tween",
ease: "backInOut",
duration: 2,
},
},
};
//
<React.Fragment>
<motion.div
variants={componentVariants}
initial="hidden"
animate="show"
exit="exit"
style={{
opacity: 1,
}}
>
{data &&
data.map((animation) => {
const { name, component } = animation;
let animationComponents;
if (Array.isArray(component)) {
animationComponents = (
<>
{component.map((c, i) => {
const AnimationComponent = c;
return (
<AnimationComponent
key={name + i}
/>
);
})}
</>
);
} else {
const SingleComponent = component;
animationComponents = <SingleComponent />;
}
return (
<div
key={name}
className="container"
style={{
height: animation.containerHeight ?? "auto",
}}>
<h3>{name}</h3>
<InitialButton>
{animationComponents}
</InitialButton>
</div>
);
})}
</motion.div>
<motion.div
layout="size"
className="transitionOverlay"
variants={containerVariants}
initial="hidden"
animate={"exit"}
exit={"exit"}
/>
</React.Fragment>
在 react-router 跟一般的元件其實操作是一樣的,同樣也是換掉元件的概念哦 ! 有點來不及補 mode 的部份,晚點會在補在這篇。
沒想到我能到第 10 天,再...再 2 個 10 天 ! 所有的文章內容也正在滾動式修正了 QQ,如果有看到新變動都是很正常的。